using System;
using System.Collections.Generic;
using System.Linq;
using Telerik.Web.Mvc;
using TelerikMvcGridCustomBindingHelper.Exceptions;
using TelerikMvcGridCustomBindingHelper.Mapper;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.DynamicQuery
{
    internal abstract class BaseDynamicWhereClause<TEntity, TViewModel, TPredicate>
        where TEntity : class
        where TViewModel : class
    {
        private readonly CaseInsensitive<TEntity> _caseInsensitive;
        private readonly bool _queryAcceptNullValuesWhenFiltering;
        private readonly ValueResolvers _queryValueResolvers;
        protected readonly FilterDescriptor InnerFilterDescriptor;
        protected readonly object ConvertedValue;
        public GridPropertyInfo GridPropertyInfo { get; private set; }
        public TPredicate Predicate { get; private set; }
        public string TargetProperty { get { return GridPropertyInfo == null ? null : GridPropertyInfo.PropertyPath; } }

        protected BaseDynamicWhereClause() { }

        protected BaseDynamicWhereClause(FilterDescriptor filterDescriptor, CaseInsensitive<TEntity> caseInsensitive, bool queryAcceptNullValuesWhenFiltering, ValueResolvers queryValueResolvers)
        {
            _queryValueResolvers = queryValueResolvers;
            InnerFilterDescriptor = filterDescriptor;
            GridPropertyInfo = GetGridPropertyInfo(filterDescriptor.Member);
            ConvertedValue = ResolveValue(filterDescriptor.Value);
            _queryAcceptNullValuesWhenFiltering = queryAcceptNullValuesWhenFiltering;

            _caseInsensitive = new CaseInsensitive<TEntity>();

            if (caseInsensitive != null)
            {
                _caseInsensitive = caseInsensitive;
            }
            else
            {
                _caseInsensitive.CopyFrom<object>(GridModelMapper.ConfigurationObject.GlobalCaseInsensitiveSearchOptions);
            }

            InitializePredicate();
        }

        protected abstract TPredicate OrElse(TPredicate left, TPredicate right);

        protected abstract TPredicate AndAlso(TPredicate left, TPredicate right);

        public virtual bool HasFilters()
        {
            return Equals(Predicate, null) == false;
        }

        private void InitializePredicate()
        {
            var shouldAcceptNullValuesWhenFiltering = ShouldAcceptNullValuesWhenFiltering();
            var predicateThatAcceptNullValues = default(TPredicate);

            var hasNullSubstitutes = GridModelMapper.ConfigurationObject.NullSubstitutes.HasSubstituteFor<TEntity, TViewModel>(GridPropertyInfo, ConvertedValue);
            var hasNullAliases = GridModelMapper.ConfigurationObject.NullAliases.HasAliasFor<TEntity, TViewModel>(GridPropertyInfo, ConvertedValue);
            var nullSubstitutes = GridModelMapper.ConfigurationObject.NullSubstitutes.GetSubstitutesFor<TEntity, TViewModel>(GridPropertyInfo);

            var filterOperator = EnforceEqualityOperator();

            if (ConvertedValue != null && shouldAcceptNullValuesWhenFiltering || (hasNullSubstitutes || hasNullAliases))
            {
                var shouldUseNotEqualsOperator = shouldAcceptNullValuesWhenFiltering == false && filterOperator == FilterOperator.IsNotEqualTo;
                predicateThatAcceptNullValues = BuildPredicateThatAcceptNullValues(nullSubstitutes, shouldUseNotEqualsOperator);
            }

            var predicate = default(TPredicate);

            if (hasNullAliases)
            {
                var andAlso = filterOperator == FilterOperator.IsNotEqualTo;
                var nullAliases = GridModelMapper.ConfigurationObject.NullAliases.GetAliasFor<TEntity, TViewModel>(GridPropertyInfo);
                if (nullAliases.SearchExplicit)
                {
                    foreach (var nullAlias in nullAliases.Aliases().Where(o => o != null))
                    {
                        if (hasNullSubstitutes && nullSubstitutes.NullValues().Any(o => Equals(o, nullAlias)))
                            continue;

                        var value = nullAlias;

                        if (GridPropertyInfo.PropertyType != nullAlias.GetType())
                        {
                            if (nullAlias.GetType().IsValueType && GridPropertyInfo.PropertyType.IsValueType)
                            {
                                value = Reflector.TryChangeType(value, GridPropertyInfo.PropertyType);
                            }
                            else
                            {
                                continue;
                            }
                        }

                        var pred = CheckCase(value);
                        predicate = andAlso ? AndAlso(predicate, pred) : OrElse(predicate, pred);
                    }
                }

                Predicate = andAlso ? AndAlso(predicateThatAcceptNullValues, predicate) : OrElse(predicateThatAcceptNullValues, predicate);

                if (Equals(Predicate, default(TPredicate)) == false)
                    return;
            }

            if (hasNullSubstitutes)
            {
                Predicate = predicateThatAcceptNullValues;
                return;
            }

            var booleanSubstitutes = GridModelMapper.ConfigurationObject.BooleanSubstitutes;

            var hasBooleanSubstitutes = booleanSubstitutes.HasSubstituteFor<TEntity, TViewModel>(GridPropertyInfo);

            if (hasBooleanSubstitutes && (ConvertedValue is bool || ConvertedValue == null))
            {
                var value = ConvertedValue is bool ? (bool?)ConvertedValue : null;

                var substitutes = booleanSubstitutes.GetSubstitutesFor<TEntity, TViewModel>(GridPropertyInfo, value);
                predicate = BuildPredicateWithBooleanSubstitutes(substitutes);
            }
            else
            {
                predicate = CheckCase(ConvertedValue);
            }

            Predicate = OrElse(predicateThatAcceptNullValues, predicate);
        }

        private TPredicate CheckCase(object value)
        {
            TPredicate predicate;

            var shouldBeCaseSensitive = ShouldBeCaseSensitive();

            if ((_caseInsensitive.ShouldBeOnClientSide && shouldBeCaseSensitive) &&
                (GridPropertyInfo.PropertyType == typeof(string) && value != null))
            {
                value = ((string)value).ToUpper(GridModelMapper.ConfigurationObject.DefaultCulture);
            }

            if ((_caseInsensitive.ShouldBeOnServerSide && shouldBeCaseSensitive) &&
                (GridPropertyInfo.PropertyType == typeof(string) && value != null))
            {
                predicate = BuildPredicateCaseInsensitive((string)value);
            }
            else
            {
                predicate = BuildPredicateCaseSensitive(value);
            }

            return predicate;
        }

        private bool ShouldBeCaseSensitive()
        {
            var shouldIgnore = new PropertyPathVisitor();
            foreach (var expression in _caseInsensitive.PropertiesToIgnore)
            {
                shouldIgnore.Visit(expression);
            }

            if (shouldIgnore.Properties.Any(p => p == GridPropertyInfo.PropertyPath))
            {
                return false;
            }

            var shouldUse = new PropertyPathVisitor();
            foreach (var expression in _caseInsensitive.PropertiesToUse)
            {
                shouldUse.Visit(expression);
            }

            if (shouldUse.Properties.Any(p => p == GridPropertyInfo.PropertyPath))
            {
                return true;
            }

            return shouldUse.Properties.Any() == false;
        }

        protected FilterOperator EnforceEqualityOperator()
        {
            var filterOperator = InnerFilterDescriptor.Operator;

            switch (filterOperator)
            {
                case FilterOperator.IsLessThan:
                case FilterOperator.IsLessThanOrEqualTo:
                case FilterOperator.IsGreaterThanOrEqualTo:
                case FilterOperator.IsGreaterThan:
                case FilterOperator.StartsWith:
                case FilterOperator.EndsWith:
                case FilterOperator.Contains:
                case FilterOperator.IsEqualTo:
                    filterOperator = FilterOperator.IsEqualTo;
                    break;
                case FilterOperator.DoesNotContain:
                case FilterOperator.IsNotEqualTo:
                    filterOperator = FilterOperator.IsNotEqualTo;
                    break;
                case FilterOperator.IsContainedIn:
                    break;
                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported.", InnerFilterDescriptor.Operator));
            }

            return filterOperator;
        }

        private bool ShouldAcceptNullValuesWhenFiltering()
        {
            // Test for global AcceptNullValuesWhenFiltering
            var acceptNullValuesWhenFiltering = GridModelMapper.ConfigurationObject.ShouldAcceptNullValuesWhenFiltering;

            // Test for entity AcceptNullValuesWhenFiltering
            acceptNullValuesWhenFiltering = GridModelMapper.ConfigurationObject.AcceptNullValuesWhenFilteringType.TestType<TEntity>() || acceptNullValuesWhenFiltering;

            // Test for query AcceptNullValuesWhenFiltering
            acceptNullValuesWhenFiltering = _queryAcceptNullValuesWhenFiltering || acceptNullValuesWhenFiltering;

            // Test for property AcceptNullValuesWhenFiltering
            acceptNullValuesWhenFiltering = GridPropertyInfo.AcceptNullValuesWhenFiltering || acceptNullValuesWhenFiltering;

            return acceptNullValuesWhenFiltering;
        }

        private object ResolveValue(object value)
        {
            var valueResolvers = _queryValueResolvers;

            var hasResolver = valueResolvers != null && valueResolvers.HasResolverFor<TEntity, TViewModel>(GridPropertyInfo, value != null ? value.GetType() : null, true);

            if (hasResolver)
            {
                return valueResolvers.Resolve<TEntity, TViewModel>(GridPropertyInfo, value, true);
            }

            valueResolvers = GridModelMapper.ConfigurationObject.ValueResolvers;

            hasResolver = valueResolvers != null && valueResolvers.HasResolverFor<TEntity, TViewModel>(GridPropertyInfo, value != null ? value.GetType() : null);

            if (hasResolver)
            {
                return valueResolvers.Resolve<TEntity, TViewModel>(GridPropertyInfo, value);
            }

            var hasSubstituteFor = GridModelMapper.ConfigurationObject.BooleanSubstitutes.HasSubstituteFor<TEntity, TViewModel>(GridPropertyInfo);

            if (hasSubstituteFor || (value != null && value.GetType() == GridPropertyInfo.PropertyType))
            {
                return value;
            }

            return Reflector.TryChangeType(value, GridPropertyInfo.PropertyType);
        }

        private static GridPropertyInfo GetGridPropertyInfo(string viewModelPropertyName)
        {
            var gridPropertyInfo = GridModelMapper.GetPropertyMap<TEntity, TViewModel>(viewModelPropertyName);

            Guard.IfIsNull(gridPropertyInfo).Throw<PropertyNotFoundException>(typeof(TEntity), viewModelPropertyName);

            return gridPropertyInfo;
        }

        protected void MergePredicateFrom(BaseDynamicWhereClause<TEntity, TViewModel, TPredicate> dynamicWhereClause, bool useOrElse)
        {
            GridPropertyInfo = dynamicWhereClause.GridPropertyInfo;

            if (Equals(Predicate, default(TPredicate)))
            {
                Predicate = dynamicWhereClause.Predicate;
                return;
            }

            if (Equals(dynamicWhereClause.Predicate, default(TPredicate)))
            {
                return;
            }

            Predicate = useOrElse ? OrElse(Predicate, dynamicWhereClause.Predicate) : AndAlso(Predicate, dynamicWhereClause.Predicate);
        }

        protected abstract TPredicate BuildPredicateWithBooleanSubstitutes(IEnumerable<object> booleanSubstitutes);

        protected abstract TPredicate BuildPredicateCaseSensitive(object value);

        protected abstract TPredicate BuildPredicateCaseInsensitive(string value);

        protected abstract TPredicate BuildPredicateThatAcceptNullValues(INullSubstitute nullSubstitutes, bool useNotEquals);
    }
}